package cn.tedu.lbs.marketing.web.service;

import cn.tedu.lbs.basic.api.BasicApi;
import cn.tedu.lbs.marketing.web.repository.ActivityRepository;
import cn.tedu.lbs.marketing.web.repository.CouponCodeRepository;
import cn.tedu.lbs.marketing.web.repository.CouponRepository;
import cn.tedu.lbs.marketing.web.repository.UserCouponRepository;
import cn.tedu.lbs.marketing.web.util.AuthenticationContextUtil;
import cn.tedu.lbs.stock.api.StockApi;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.tarena.lbs.attach.api.AttachApi;
import com.tarena.lbs.base.common.utils.Asserts;
import com.tarena.lbs.base.protocol.exception.BusinessException;
import com.tarena.lbs.base.protocol.pager.PageResult;
import com.tarena.lbs.common.passport.principle.UserPrinciple;
import com.tarena.lbs.pojo.attach.dto.AttachQrDTO;
import com.tarena.lbs.pojo.attach.param.AttachQRParam;
import com.tarena.lbs.pojo.basic.dto.UserDTO;
import com.tarena.lbs.pojo.marketing.param.UserCouponsParam;
import com.tarena.lbs.pojo.marketing.po.ActivityPO;
import com.tarena.lbs.pojo.marketing.po.CouponCodePO;
import com.tarena.lbs.pojo.marketing.po.CouponPO;
import com.tarena.lbs.pojo.marketing.po.UserCouponsPO;
import com.tarena.lbs.pojo.marketing.query.UserCouponCodeQuery;
import com.tarena.lbs.pojo.marketing.query.UserCouponQuery;
import com.tarena.lbs.pojo.marketing.vo.UserCouponsVO;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import java.util.stream.Collectors;

@Service
@Slf4j
public class UserCouponService {
    @DubboReference
    private BasicApi basicApi;
    @DubboReference
    private AttachApi attachApi;
    @DubboReference
    private StockApi stockApi;
    @Autowired
    private ActivityRepository activityRepository;
    @Autowired
    private CouponRepository couponRepository;
    @Autowired
    private UserCouponRepository userCouponRepository;
    @Autowired
    private CouponCodeRepository couponCodeRepository;
    @Autowired
    private RedisTemplate redisTemplate;
    public void takeCoupon(UserCouponsParam param) throws BusinessException, ExecutionException, InterruptedException {
        log.info("当前业务主线程:{}",Thread.currentThread().getName());
        //1.检查入参中 传递的userId对应的用户是否合法
        //重构 异步调用 单独线程调用
        //get是实现逻辑 中异步线程执行的代码 可以在里面调用业务方法
        CompletableFuture<String> checkUserFuture= CompletableFuture.supplyAsync(() -> {
            try {
                checkUser(param.getUserId());
            } catch (Exception e) {
                log.error("用户校验失败", e);
                throw new RuntimeException("检查用户失败");
            }
            return "finish";
        });
        //2.检查入参中 传递的activityId对应的活动是否有效 是否合法 并验证目标人群
        CompletableFuture<String> checkActivityFuture= CompletableFuture.supplyAsync(() -> {
            try {
                checkActivity(param.getActivityId(),param.getUserId());
            } catch (Exception e) {
                log.error("用户校验失败", e);
                throw new RuntimeException("检查用户失败");
            }
            return "finish";
        });
        //3.检查入参中 传递的couponId对应优惠券是否合法 并且用户领取优惠券次数上限
        CompletableFuture<CouponPO> checkCouponFuture= CompletableFuture.supplyAsync(() -> {
            CouponPO po=null;
            try {
                po=checkAndGetCoupon(param.getCouponId(),param.getUserId());
            } catch (Exception e) {
                log.error("用户校验失败", e);
                throw new RuntimeException("检查用户失败");
            }
            return po;
        });
        //如果上述 验证查询 全部通过 允许用户进入领取优惠券操作流程
        //为了保证 当前核心业务 大致步骤 先校验 后领取 领取操作和校验结果有强关联
        //让主线程 在执行到当前行的时候,线程等待阻塞 阻塞等待反馈结果
        CompletableFuture<Void> allFuture = CompletableFuture.allOf(checkCouponFuture, checkUserFuture, checkActivityFuture);
        CouponPO coupon = checkCouponFuture.get();
        checkActivityFuture.get();
        checkUserFuture.get();
        //4.优惠券领取 锁要保护的业务方法 所的可见范围
        String lockKey="lbs:marketing:coupon:"+param.getCouponId();
        String lockValue= UUID.randomUUID().toString();
        //执行setnx命令 setnx set get incr都是写 string类型的数据
        ValueOperations<String,String> opsString = redisTemplate.opsForValue();
        Boolean tryLock =false;
        int count=0;
        do{
            if(count<3&&count>0){
                //抢了0 1 2  抢几次了
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }else if (count>=3){
                return;
            }
            tryLock=opsString.setIfAbsent(lockKey, lockValue,2, TimeUnit.SECONDS);
            count++;
        }while(!tryLock);
        //只有拿到返回结果是true才算是抢到了锁
        try{
            //说明这个线程抢到了锁
            doReceiveCoupon(param,coupon);
        }catch (Exception e){
            log.error("优惠券领取异常",e);
        }finally {
            //释放资源的操作 不能在业务后执行 因为也可能会抛异常 导致释放没执行
            String value = opsString.get(lockKey);
            if (value!=null&&value.equals(lockValue)){
                redisTemplate.delete(lockKey);
            }
        }

    }

    private void doReceiveCoupon(UserCouponsParam param, CouponPO coupon) throws BusinessException {
        long start = System.currentTimeMillis();
        //作为领取优惠券数据的操作 核心功能 创建券码 生成领取数据记录
        //判断 每人每次领取一个优惠券券码,剩余库存必须?0的.检查库存充足
        checkStockEnough(coupon.getId());
        //创建一个优惠券券码 并获取券码 券码的状态 0未领取 未分配 1已领取 已分配
        CouponCodePO couponCode=createAndGetCouponCode(coupon);
        //组织数据 记录 用户领取优惠券
        createUserCoupon(param,coupon,couponCode);
        //修改券码状态
        updateCouponCodeStatus(couponCode.getId(),1);
        //到此为止 marketing服务本地 业务数据就完成了
        //扣减库存 远程调用
        reduceCouponStock(coupon.getId());
        long end = System.currentTimeMillis();
        log.info("领取优惠券耗时:{}ms",(end-start));
    }

    private void reduceCouponStock(Integer couponId) {
        //初步设计 减库存逻辑
        stockApi.updateCouponStock(couponId,1);
    }

    private void updateCouponCodeStatus(Integer couponCodeId, Integer status) {
        //update coupon_code set status=? where id=?
        CouponCodePO updatePO=new CouponCodePO();
        updatePO.setId(couponCodeId);
        updatePO.setStatus(status);
        couponCodeRepository.updateById(updatePO);
    }

    private void createUserCoupon(UserCouponsParam param, CouponPO coupon, CouponCodePO couponCode) {
        //创建一个userCouponPO 补充必要属性 写入到数据库 了解数据库字段
        UserCouponsPO po = new UserCouponsPO();
        //1.关联优惠券
        po.setCouponId(coupon.getId());
        //2.关联优惠券券码
        po.setCouponCode(couponCode.getCouponCode());
        //3.关联用户
        po.setUserId(param.getUserId());
        //4.领取渠道 1 消息短链接 2 活动推广 3 店铺扫码 4 实体下发
        po.setReceiveChannel(param.getReceiveChannel());
        //5.店铺id
        po.setShopId(param.getShopId());
        //6.活动id
        po.setActivityId(param.getActivityId());
        //7.优惠券类型 优惠券优惠金额
        po.setCouponType(coupon.getCouponType());
        po.setCouponValue(coupon.getDiscountValue());
        //8.优惠券领取数据状态 0已领取 1已使用 2已过期
        po.setStatus(0);
        //9.时间值
        po.setCreateAt(new Date());
        po.setUpdateAt(new Date());
        userCouponRepository.save(po);
    }

    private CouponCodePO createAndGetCouponCode(CouponPO coupon) {
        //创建一个couponCodePO 补充必要属性 写入到数据库
        CouponCodePO po = new CouponCodePO();
        //1.关联的优惠券
        po.setCouponId(coupon.getId());
        //2.唯一编码 UUID
        po.setCouponCode(UUID.randomUUID().toString());
        //3.和优惠券保持一致 关联商家
        po.setBusinessId(coupon.getBusinessId());
        //4.初始化状态 0 未领取
        po.setStatus(0);
        //5.通用字段属性 创建和修改时间
        po.setCreateAt(new Date());
        po.setUpdateAt(new Date());
        couponCodeRepository.save(po);
        return po;
    }

    private void checkStockEnough(Integer couponId) throws BusinessException {
        //告诉库存服务 哪个优惠券的库存 检查库存数量几个
        Integer num = stockApi.getCouponStock(couponId);
        log.info("领取优惠券:检查库存是否充足,当前库存剩余数量:{}",num);
        //0 大于0 不能小于0 小于0代表库存超领
        Asserts.isTrue(num==0,new BusinessException("-2","优惠券库存不足"));
    }

    private CouponPO checkAndGetCoupon(Integer couponId, Integer userId) throws BusinessException {
        log.info("检查用户合法线程:{}",Thread.currentThread().getName());
        long start = System.currentTimeMillis();
        log.info("领取优惠券:校验优惠券合法性入参:{}",couponId);
        //读取优惠券数据信息
        CouponPO coupon = couponRepository.getById(couponId);
        log.info("领取优惠券:校验优惠券合法性入参:{},校验结果:{}",couponId,coupon);
        Asserts.isTrue(coupon==null,new BusinessException("-2","优惠券不存在"));
        //优惠券存在的情况下,我们需要检查当前用户是否已经领取优惠券达到上限了
        //查询领取记录 该用户领取该优惠券的次数 对比优惠券数据中最大领取上限
        //select count(1) from user_coupons where user_id=? and coupon_id=?
        QueryWrapper queryWrapper = new QueryWrapper();
        queryWrapper.eq("user_id",userId);
        queryWrapper.eq("coupon_id",couponId);

        long count = userCouponRepository.count(queryWrapper);
        Integer limit = coupon.getUsageLimit();
        log.info("领取优惠券:校验优惠券领取次数校验,已领取:{},上限数字:{}",count,limit);
        Asserts.isTrue(count>=limit,new BusinessException("-2","用户领取优惠券已达上限"));
        long end = System.currentTimeMillis();
        log.info("校验优惠券合法性耗时:{}MS",end-start);
        return coupon;
    }

    private void checkUser(Integer userId) throws BusinessException {
        log.info("检查用户合法线程:{}",Thread.currentThread().getName());
        long start=System.currentTimeMillis();
        log.info("领取优惠券:校验用户合法性入参:{}",userId);
        //当前服务 并没有任何小程序用户的详细信息 没法验证
        //远程调用basicApi.设计 对外暴露一个接口 利用userId 查询user详情
        UserDTO userDetail = basicApi.getUserDetail(userId);
        log.info("领取优惠券:校验用户合法性入参:{},校验查询结果:{}",userId,userDetail);
        //当前user中 没有其他的一些判断合法字段 比如冻结 启用禁用 超时等等
        Asserts.isTrue(userDetail==null,new BusinessException("-2","领取优惠券用户不存在"));
        long end=System.currentTimeMillis();
        log.info("校验用户合法性耗时:{}MS",end-start);
    }

    private void checkActivity(Integer activityId, Integer userId) throws BusinessException {
        log.info("检查用户合法线程:{}",Thread.currentThread().getName());
        long startTime=System.currentTimeMillis();
        log.info("领取优惠券:校验活动合法性入参:{}",activityId);
        //读取本地数据库的活动数据 判断合法 非空 时间是否满足要求 当前系统时间大于start 小于end
        ActivityPO activity = activityRepository.getById(activityId);
        log.info("领取优惠券:校验活动合法性入参:{},校验查询结果",activityId,activity);
        Asserts.isTrue(activity==null,new BusinessException("-2","领取优惠券活动不存在"));
        //判断 活动时间范围 是否在当前系统时间范围内
        Date start=activity.getStartDate();
        Date end=activity.getEndDate();
        Date now=new Date();
        boolean isOnTime=start.before(now)&&end.after(now);
        Asserts.isTrue(!isOnTime,new BusinessException("-2","活动不在时间范围内"));
        //判断领取优惠券的用户是否 是目标人群的范围
        //准备判断结果
        boolean isTargetCustomer=false;
        //查询活动中是否存在目标人群 如果没有目标人群 面相所有人
        String targetCustomer = activity.getTargetCustomer();
        log.info("领取优惠券:校验用户满足目标人群:{}",targetCustomer);
        if (targetCustomer!=null){
            //有目标人群 调用basicApi查询当前用户 所属人群
            List<Integer> userGroupIds = basicApi.getUserGroupIds(userId, activity.getBusinessId());
            //判断用户所属人群是否是空
            if (userGroupIds!=null&&userGroupIds.size()>0){
                isTargetCustomer=userGroupIds.contains(Integer.valueOf(targetCustomer));
            }
            //说明用户 没有在当前商家下存在任何所属人群
        }else{
            isTargetCustomer=true;
        }
        log.info("领取优惠券:校验用户满足目标人群:{},校验结果:{}",targetCustomer,isTargetCustomer);
        Asserts.isTrue(!isTargetCustomer,new BusinessException("-2","用户不在目标人群中"));
        long endTime=System.currentTimeMillis();
        log.info("校验活动合法性耗时:{}MS",endTime-startTime);
    }

    public PageResult<UserCouponsVO> myCoupons(UserCouponQuery couponQuery) throws BusinessException {
        //1.解析用户认证
        Integer userId = checkAndGetUserId();
        //2.读取我的优惠券 满足入参 状态要求
        List<UserCouponsPO> pos=getMyCoupons(couponQuery,userId);
        //3.准备一个返回值PageResult
        PageResult<UserCouponsVO> result=new PageResult<>(10,1);
        result.setTotal(100L);
        if (pos!=null&&pos.size()>0){
            result.setObjects(pos.stream().map(po -> {
                UserCouponsVO vo = new UserCouponsVO();
                BeanUtils.copyProperties(po, vo);
                //UNDO 用户领取的优惠券vo 缺少几个属性 比如 优惠券标题 使用couponId 查询couponRepository
                //用户同一张优惠券领取3次,使用过滤 去重方式 对应同一个优惠券 多个券码 只查询依次标题
                return vo;
            }).collect(Collectors.toList()));
        }
        return result;
    }

    private List<UserCouponsPO> getMyCoupons(UserCouponQuery couponQuery, Integer userId) {
        //select * from user_coupons where user_id=? and status=?
        QueryWrapper<UserCouponsPO> queryWrapper=new QueryWrapper<>();
        queryWrapper.eq("user_id",userId);
        queryWrapper.eq("status",couponQuery.getStatus());
        return userCouponRepository.list(queryWrapper);
    }

    private Integer checkAndGetUserId() throws BusinessException {
        UserPrinciple userPrinciple = AuthenticationContextUtil.get();
        Asserts.isTrue(userPrinciple==null,new BusinessException("-2","用户认证解析失败"));
        return userPrinciple.getId();
    }

    public UserCouponsVO myCouponDetail(UserCouponCodeQuery query) {
        //select * from user_coupons where coupon_code=?
        //如果考虑一个复杂的业务场景 张三领了之后 退了 李四又领取
        //1.查询 用户领取记录详情 userCouponsPO
        UserCouponsPO po=getMyCoupon(query.getCouponCode());
        //2.转化VO 封装缺少数据
        UserCouponsVO vo=null;
        if (po!=null){
            vo=new UserCouponsVO();
            BeanUtils.copyProperties(po,vo);
            //3. 生成当前券码的 二维码url地址
            String url=generateQRUrl(vo.getCouponCode(),vo.getCouponId());
            vo.setCouponUrl(url);
        }
        return vo;
    }

    private String generateQRUrl(String couponCode,Integer bizId) {
        //补充一个优惠券券码 二维码生成获取的缓存逻辑
        //1.准备 key值 和操作redis的客户端对象
        String urlKey="lbs:marketing:coupon:url:"+couponCode;
        ValueOperations<String,String> opsForValue = redisTemplate.opsForValue();
        //2.查询 缓存
        String qrUrl = opsForValue.get(urlKey);
        if (qrUrl!=null){
            log.info("优惠券券码 二维码url缓存命中,key:{}",urlKey);
            //缓存命中
            return qrUrl;
        }else{
            log.info("优惠券券码 二维码url缓存没命中,key:{}",urlKey);
            //缓存没命中
            //generateQrCode 生成二维码图片的方法
            AttachQRParam param = new AttachQRParam();
            //入参 2个属性 bizId二维码绑定的业务数据 bizType=800表示优惠券二维码图片
            param.setBusinessId(bizId);
            param.setBusinessType(800);
            //String content 你想要在二维码里存放携带的数据 优惠券券码 就是携带的内容
            String content="https://www.deepseek.com?couponCode="+couponCode;
            param.setContent(content);
            AttachQrDTO attachQrDTO = attachApi.generateQrCode(param);
            //将图片url地址存储到redis缓存中
            opsForValue.set(urlKey,attachQrDTO.getUrl());
            //从dto拿到图片连接地址url
            return attachQrDTO.getUrl();
        }
    }

    private UserCouponsPO getMyCoupon(String couponCode) {
        QueryWrapper<UserCouponsPO> queryWrapper=new QueryWrapper<>();
        queryWrapper.eq("coupon_code",couponCode);
        return userCouponRepository.getOne(queryWrapper);
    }
}
